package tournamentTree;

import java.util.LinkedList;

import com.mxgraph.model.mxCell;

/**
 * Class representing a totally balanced binary tree structure
 */
public class TournamentNode {

	/* reference to the mxCell that represents the node in the graph */
	private mxCell graphNode;
	/* reference to the upper child node */
	private TournamentNode upperChild;
	/* reference to the the lower child node */
	private TournamentNode lowerChild;
	/* reference to the parent node */
	private TournamentNode parent;

	/* bowCathegory the node refers to */
	private final String bowCathegory;
	/* the node ID */
	private final int nodeID;
	/* player data */
	private MatchData matchData;

	/**
	 * @param bowCathegory
	 *            the corresponding bow cathegory references to other nodes will
	 *            be initialized with null
	 */
	public TournamentNode(String bowCathegory) {
		this.upperChild = null;
		this.lowerChild = null;
		this.parent = null;
		this.bowCathegory = bowCathegory;
		this.nodeID = TreeIdGenerator.getInstance().getNextID();
		this.matchData = new MatchData();
	}

	/**
	 * @param depth
	 *            the depth of the tree to be generated
	 * @param bowCathegory
	 *            the corresponding bow cathegory
	 */
	public TournamentNode(double depth, String bowCathegory) {
		this(bowCathegory);
		while (this.getMaxDepth() < depth) {
			this.addChildren();
		}
	}

	/**
	 * @param bowCathegory
	 *            the corresponding bow cathegory
	 * @param participants
	 *            list of all participants in descending order according to
	 *            their total score
	 */
	public TournamentNode(String bowCathegory, LinkedList < Integer > participants) {
		this((Math.log(participants.size()) / Math.log(2)), bowCathegory);
	}

	/**
	 * @return the id of this node
	 */
	public int getID() {
		return this.nodeID;
	}

	/**
	 * @param parent
	 *            reference to the parent of this node
	 */
	private void setParent(TournamentNode parent) {
		this.parent = parent;
	}

	/**
	 * adds an upper and a lower child to each leaf node -> tree remains
	 * perfectly balanced
	 */
	public void addChildren() {
		if (this.isLeaf()) {
			this.upperChild = new TournamentNode(this.bowCathegory);
			this.upperChild.setParent(this);

			this.lowerChild = new TournamentNode(this.bowCathegory);
			this.lowerChild.setParent(this);
		} else {
			this.upperChild.addChildren();
			this.lowerChild.addChildren();
		}
	}

	/**
	 * @return the reference to the node's parent
	 */
	public TournamentNode getParent() {
		return this.parent;
	}

	/**
	 * @return the reference to the node's lower child
	 */
	public TournamentNode getLowerChild() {
		return this.lowerChild;
	}

	/**
	 * @return the reference to the node's upper child
	 */
	public TournamentNode getUpperChild() {
		return this.upperChild;
	}

	/**
	 * @return true if the node is a leaf-node, false otherwise
	 */
	public boolean isLeaf() {
		if ((this.upperChild == null) && (this.lowerChild == null)) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * @return the layer the node is in (starting with depth 0 for the root
	 *         node)
	 */
	public int getCurrentDepth() {
		int depth = 0;
		TournamentNode current = this;
		while (!current.isRoot()) {
			depth++;
			current = current.getParent();
		}
		return depth;
	}

	/**
	 * @return true if this is the root node, false otherwise
	 */
	public boolean isRoot() {
		if (this.parent == null) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * @return the depth of the tree;
	 */
	public int getMaxDepth() {
		int depth = 0;
		TournamentNode node = this.getRoot();

		while (!node.isLeaf()) {
			depth++;
			node = node.getUpperChild();
		}
		return depth;
	}

	/**
	 * @return the horizontal position of this node in the layer of the tree
	 *         (which is right-to-left-sided)
	 */
	public int getPositionInLayer() {
		LinkedList < Integer > nodeIdList = (this.getRoot()).getNodesOfSameLayer(this, new LinkedList < Integer >());
		for (int i = 0; i < nodeIdList.size(); i++) {
			if (nodeIdList.get(i) == this.getID()) {
				return i;
			}
		}

		return -1;
	}

	/**
	 * @param toCheck
	 *            the node to be analyzed
	 * @param sameLayerNodes
	 *            a list of all nodes which are in the same layer (first
	 *            function call with empty list)
	 * @return a list of all nodes which are located in the same layer, ordered
	 *         from top to bottom
	 */
	private LinkedList < Integer > getNodesOfSameLayer(TournamentNode toCheck, LinkedList < Integer > sameLayerNodes) {
		if (toCheck.getCurrentDepth() == this.getCurrentDepth()) {
			sameLayerNodes.add(this.getID());
		}

		if (!this.isLeaf()) {
			(this.getUpperChild()).getNodesOfSameLayer(toCheck, sameLayerNodes);
			(this.getLowerChild()).getNodesOfSameLayer(toCheck, sameLayerNodes);
		}

		return sameLayerNodes;
	}

	/**
	 * @return the root of the tree
	 */
	public TournamentNode getRoot() {
		TournamentNode current = this;
		while (!current.isRoot()) {
			current = current.getParent();
		}
		return current;
	}

	@Override
	public String toString() {
		return this.matchData.toString();
	}

	/**
	 * prints out the whole tree (System.out.println())
	 */
	public void printAllDescendants() {
		if (this.isLeaf()) {
			System.out.println(this);
		} else {
			System.out.println(this);
			(this.getUpperChild()).printAllDescendants();
			(this.getLowerChild()).printAllDescendants();
		}
	}

	/**
	 * @return reference to the GUI-graphNode
	 */
	public mxCell getGraphNode() {
		return this.graphNode;
	}

	/**
	 * @param node
	 *            reference to the GUI-graphNode
	 */
	public void setGraphNode(mxCell node) {
		this.graphNode = node;
	}

	@Override
	public boolean equals(Object anObject) {
		if (anObject instanceof TournamentNode) {
			return ((TournamentNode) anObject).getID() == this.getID();
		}
		return false;
	}

	/**
	 * @return true if the link from the parent node to this one goes upwards,
	 *         false otherwise
	 */
	public boolean isUpperChild() {
		if (this.isRoot()) {
			return true;
		} else {
			return this.getParent().getUpperChild().equals(this);
		}
	}

	/**
	 * @return true if the link from the parent node to this one goes downwards,
	 *         false otherwise
	 */
	public boolean isLowerChild() {
		if (this.isRoot()) {
			return false;
		} else {
			return this.getParent().getLowerChild().equals(this);
		}
	}

	/**
	 * @return the contained match data in this node
	 */
	public MatchData getMatchData() {
		return this.matchData;
	}

	/**
	 * to be called on root node!
	 * @param all
	 *            participants
	 */
	public void setParticipantsAtLeaves(LinkedList < Integer > participants) {

		/* build tree from root to leaves */
		LinkedList < TournamentNode > nodeList = new LinkedList < TournamentNode >();
		this.getMatchData().setPlayer(participants.getFirst());
		nodeList.add(this);

		while (nodeList.size() > 0) {
			if (!nodeList.getFirst().isLeaf()) {
				if (nodeList.getFirst().isUpperChild()) {
					(nodeList.getFirst().getUpperChild()).getMatchData().setPlayer((nodeList.getFirst().getMatchData().getAssignedPlayerID()));
					(nodeList.getFirst().getLowerChild()).getMatchData().setPlayer(this.getPlayerWithMaximalDistance(participants, (nodeList.getFirst().getUpperChild())));
					nodeList.add((nodeList.getFirst().getUpperChild()));
					nodeList.add((nodeList.getFirst().getLowerChild()));
					nodeList.removeFirst();
				} else {
					(nodeList.getFirst().getLowerChild()).getMatchData().setPlayer((nodeList.getFirst().getMatchData().getAssignedPlayerID()));
					(nodeList.getFirst().getUpperChild()).getMatchData().setPlayer(this.getPlayerWithMaximalDistance(participants, (nodeList.getFirst().getLowerChild())));
					nodeList.add((nodeList.getFirst().getLowerChild()));
					nodeList.add((nodeList.getFirst().getUpperChild()));
					nodeList.removeFirst();
				}
			} else {
				nodeList.removeFirst();
			}
		}

		/* after all nodes have been computed, reset all data in non-leaves */
		this.wipeAllNonLeaves();
	}

	/**
	 * inserts a "freilos" on every leaf no player is assigned yet
	 */
	public void insertFreilose() {
		if (!this.isLeaf()) {
			(this.getUpperChild()).insertFreilose();
			(this.getLowerChild()).insertFreilose();
		} else {
			if (this.getMatchData().getAssignedPlayerID() == 0) {
				this.getMatchData().setFreilos(true);
			}
		}
	}

	/**
	 * sets all players, which had the luck to draw a "freilos" to the next
	 * round
	 */
	public void continueFreilose() {
		if (!this.isLeaf()) {
			if ((this.getUpperChild()).getMatchData().isFreilos()) {
				this.getMatchData().setPlayer((this.getLowerChild()).getMatchData().getAssignedPlayerID());
			}
			if ((this.getLowerChild()).getMatchData().isFreilos()) {
				this.getMatchData().setPlayer((this.getUpperChild()).getMatchData().getAssignedPlayerID());
			}
			this.getUpperChild().continueFreilose();
			this.getLowerChild().continueFreilose();
		}
	}

	/**
	 * @param participants
	 *            ordered list of all participants
	 * @param toMatch
	 *            the node for which an opponent has to be returned
	 * @return the player id which is most distant for the node (corresponding
	 *         to layer and list position)
	 */
	private int getPlayerWithMaximalDistance(LinkedList < Integer > participants, TournamentNode toMatch) {

		int layer = toMatch.getCurrentDepth();

		if (layer == 0) {
			return participants.getFirst();
		} else {
			int lastValidListElement = (int) Math.pow(2, layer);

			LinkedList < Integer > alreadyInserted = new LinkedList < Integer >();
			LinkedList < Integer > newNumbers = new LinkedList < Integer >();

			for (int i = 0; i < lastValidListElement / 2; i++) {
				alreadyInserted.add(participants.get(i));
			}
			for (int i = lastValidListElement - 1; i >= lastValidListElement / 2; i--) {
				newNumbers.add(participants.get(i));

			}

			for (int i = 0; i < alreadyInserted.size(); i++) {

				if (toMatch.getMatchData().getAssignedPlayerID().equals(alreadyInserted.get(i))) {

					return newNumbers.get(i);
				}
			}

			return 0;
		}
	}

	/**
	 * resets the match data in all nodes which are not leaves
	 */
	private void wipeAllNonLeaves() {
		if (!this.isLeaf()) {
			this.matchData = new MatchData();
			if (!this.getUpperChild().isLeaf()) {
				(this.getUpperChild()).wipeAllNonLeaves();
			}
			if (!this.getLowerChild().isLeaf()) {
				(this.getLowerChild()).wipeAllNonLeaves();
			}
		}
	}
}
